﻿/*
OpenShell: An open source library that implements the Powershell engine for console applications
Copyright (C) 2011 Bryan Perris

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation.Host;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
using System.IO;

namespace OpenShell
{
    public partial class ConsoleWindowHelper
    {
        private IntPtr m_WindowHandle;

        public ConsoleWindowHelper()
        {
            GetHandles();
        }

        private void GetHandles()
        {
            m_WindowHandle = GetStdHandle(-11);
        }

        private CONSOLE_SCREEN_BUFFER_INFO GetBufferInfo()
        {
            CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO();

            if (GetConsoleScreenBufferInfo(m_WindowHandle, out info))
            {
                return info;
            }
            else
            {
                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
                throw new InvalidOperationException();
            }
        }

        private SMALL_RECT ConvertRectangle(Rectangle rectangle)
        {
            SMALL_RECT rec = new SMALL_RECT();
            rec.Bottom = (short)rectangle.Bottom;
            rec.Left = (short)rectangle.Left;
            rec.Right = (short)rectangle.Right;
            rec.Top = (short)rectangle.Top;

            return rec;
        }

        private COORD ConvertCoordinates(Coordinates coordinates)
        {
            COORD cord = new COORD();
            cord.X = (short)coordinates.X;
            cord.Y = (short)coordinates.Y;

            return cord;
        }

        private CHAR_INFO ConvertBufferCell(BufferCell cell)
        {
            CHAR_INFO charInfo = new CHAR_INFO();

            if (cell.BufferCellType == BufferCellType.Leading)
                charInfo.Attributes &= (ushort)INFO_CHAR_ATTRIBUTES.COMMON_LVB_LEADING_BYTE;
            else
                charInfo.Attributes &= (ushort)INFO_CHAR_ATTRIBUTES.COMMON_LVB_TRAILING_BYTE;

            charInfo.AsciiChar = cell.Character;

            return charInfo;
        }

        #region IConsoleWindowHelper Memebers

        public BufferCell[,] GetBufferData(Rectangle rectangle)
        {
            // Buffer Size
            COORD size = new COORD();
            size.X = (short)(rectangle.Right - rectangle.Left);
            size.Y = (short)(rectangle.Bottom - rectangle.Top);

            BufferCell[,] cells = new BufferCell[size.X, size.Y];
            CHAR_INFO[] infoSet = new CHAR_INFO[size.X * size.Y];

            // Buffer Position
            COORD pos = new COORD();
            pos.X = (short)rectangle.Left;
            pos.Y = (short)rectangle.Top;

            // Buffer Region
            SMALL_RECT rect = ConvertRectangle(rectangle);

            if (ReadConsoleOutput(m_WindowHandle, infoSet, size, pos, ref rect))
            {
                for (int x = 0; x < (int)size.X; x++)
                {
                    for (int y = 0; y < (int)size.Y; y++)
                    {
                        BufferCell cell = new BufferCell();
                        CHAR_INFO info = infoSet[(y * size.X) + x];
                        cell.Character = info.AsciiChar;

                        if ((info.Attributes & (ushort)INFO_CHAR_ATTRIBUTES.COMMON_LVB_LEADING_BYTE) == 1)
                            cell.BufferCellType = BufferCellType.Leading;
                        else
                            cell.BufferCellType = BufferCellType.Trailing;

                        
                        // Get the console colors
                        // Well fuck it, Console class limts our colors :(
                        // So we will alwasy return the current console colors
                        cell.BackgroundColor = Console.BackgroundColor;
                        cell.ForegroundColor = Console.ForegroundColor;

                        cells[x, y] = cell;
                    }
                }
            }
            else
            {
                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
            }

            return cells;

        }

        public Coordinates GetCursorPositon()
        {
            Coordinates coords = new Coordinates(-1, -1);

            CONSOLE_SCREEN_BUFFER_INFO info = GetBufferInfo();

            coords.X = (int)info.dwCursorPosition.X;
            coords.Y = (int)info.dwCursorPosition.Y;

            return coords;
        }

        public void SetCursorPosition(Coordinates position)
        {
            COORD coord = ConvertCoordinates(position);

            if (!SetConsoleCursorPosition(m_WindowHandle, coord))
            {
                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
            }
        }

        public Size GetMaxPhysicalWindowSize()
        {
            Size size = new Size(-1, -1);

            CONSOLE_SCREEN_BUFFER_INFO info = GetBufferInfo();

            size.Width = (int)info.dwMaximumWindowSize.X;
            size.Height = (int)info.dwMaximumWindowSize.Y;

            return size;
        }

        public void FlushInputStream()
        {
            if (!FlushConsoleInputBuffer(m_WindowHandle))
            {
                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
            }
        }

        public void ScrollScreenBuffer(Rectangle source, Coordinates destination, Rectangle clip, BufferCell fill)
        {
            SMALL_RECT src = ConvertRectangle(source);
            COORD dst = ConvertCoordinates(destination);
            SMALL_RECT clp = ConvertRectangle(clip);
            CHAR_INFO charInfo = ConvertBufferCell(fill);

            if (!ScrollConsoleScreenBuffer(m_WindowHandle, ref src, ref clp, dst, ref charInfo))
            {
                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
            }
        }

        public void FillBufferContents(Rectangle rectangle, BufferCell fill)
        {
            CHAR_INFO charInfo = ConvertBufferCell(fill);
            COORD size = ConvertCoordinates(new Coordinates((rectangle.Right - rectangle.Left), (rectangle.Bottom - rectangle.Top)));
            COORD pos = ConvertCoordinates(new Coordinates(rectangle.Left, rectangle.Bottom));
            SMALL_RECT region = ConvertRectangle(rectangle);

            CHAR_INFO[] chars = new CHAR_INFO[size.X * size.Y];

            for (int i = 0; i < chars.Length; i++)
            {
                chars[i] = charInfo;
            }

            if (!WriteConsoleOutput(m_WindowHandle, chars, size, pos, ref region))
            {
                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
            }
        }

        public void SetBufferContents(Coordinates origin, BufferCell[,] contents)
        {
            SMALL_RECT region = ConvertRectangle(new Rectangle(origin, new Coordinates(contents.GetLength(0), contents.GetLength(1))));
            CHAR_INFO[] chars = new CHAR_INFO[region.Right * region.Bottom];
            COORD size = ConvertCoordinates(new Coordinates((contents.GetLength(0) - origin.X), contents.GetLength(1) - origin.Y));
            COORD offset = ConvertCoordinates(origin);

            for (int x = origin.X; x < contents.GetLength(0); x++)
            {
                for (int y = origin.Y; y < contents.GetLength(1); y++)
                {
                    chars[(y * size.X) + x] = ConvertBufferCell(contents[x, y]);
                }
            }

            if (!WriteConsoleOutput(m_WindowHandle, chars, size, offset, ref region))
            {
                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
            }
        }

        #endregion

        #region Win32 Functions

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern IntPtr GetStdHandle(int nStdHandle);

        [DllImport("kernel32.dll")]
        private static extern bool GetConsoleScreenBufferInfo(IntPtr hConsoleOutput,
           out CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo);

        [DllImport("kernel32.dll")]
        private static extern bool ReadConsoleOutput(IntPtr hConsoleOutput, [Out] CHAR_INFO[]
           lpBuffer, COORD dwBufferSize, COORD dwBufferCoord,
           ref SMALL_RECT lpReadRegion);

        [DllImport("kernel32.dll")]
        private static extern bool GetConsoleCursorInfo(IntPtr hConsoleOutput,
           out CONSOLE_CURSOR_INFO lpConsoleCursorInfo);

        [DllImport("kernel32.dll")]
        private static extern bool SetConsoleCursorPosition(IntPtr hConsoleOutput,
           COORD dwCursorPosition);

        [DllImport("kernel32.dll")]
        private static extern bool FlushConsoleInputBuffer(IntPtr hConsoleInput);

        [DllImport("kernel32.dll")]
        private static extern bool ScrollConsoleScreenBuffer(IntPtr hConsoleOutput,
           [In] ref SMALL_RECT lpScrollRectangle, [In] ref SMALL_RECT lpClipRectangle,
           COORD dwDestinationOrigin, [In] ref CHAR_INFO lpFill);

        [DllImport("kernel32.dll")]
        private static extern bool WriteConsoleOutput(IntPtr hConsoleOutput, CHAR_INFO[]
           lpBuffer, COORD dwBufferSize, COORD dwBufferCoord, ref SMALL_RECT
           lpWriteRegion);

        #endregion

        #region Win32 Types

        [StructLayout(LayoutKind.Sequential)]
        private struct CONSOLE_CURSOR_INFO
        {
            uint Size;
            bool Visible;
        }

        private struct COORD
        {
            public short X;
            public short Y;
        }

        private struct SMALL_RECT
        {
            public short Left;
            public short Top;
            public short Right;
            public short Bottom;
        }

        private struct CONSOLE_SCREEN_BUFFER_INFO
        {
            public COORD dwSize;
            public COORD dwCursorPosition;
            public short wAttributes;
            public SMALL_RECT srWindow;
            public COORD dwMaximumWindowSize;
        }

        [Flags]
        private enum INFO_CHAR_ATTRIBUTES : ushort
        {
            FOREGROUND_BLUE = 0x0001,
            FOREGROUND_GREEN = 0x0002,
            FOREGROUND_RED = 0x0004,
            FOREGROUND_INTENSITY = 0x0008,
            BACKGROUND_BLUE = 0x0010,
            BACKGROUND_GREEN = 0x0020,
            BACKGROUND_RED = 0x0040,
            BACKGROUND_INTENSITY = 0x0080,
            COMMON_LVB_LEADING_BYTE = 0x0100,
            COMMON_LVB_TRAILING_BYTE = 0x0200,
            COMMON_LVB_GRID_HORIZONTAL= 0x0400,
            COMMON_LVB_GRID_LVERTICAL = 0x0800,
            COMMON_LVB_GRID_RVERTICAL = 0x1000,
            COMMON_LVB_REVERSE_VIDEO = 0x4000,
            COMMON_LVB_UNDERSCORE = 0x8000
        }

        [StructLayout(LayoutKind.Explicit)]
        private struct CHAR_INFO
        {
            [FieldOffset(0)]
            internal char UnicodeChar;
            [FieldOffset(0)]
            internal char AsciiChar;
            [FieldOffset(2)] //2 bytes seems to work properly
            internal UInt16 Attributes;
        }

        #endregion
    }
}
